import { Container } from '../interface';
import { dom, EventHandler, Nodes } from '@knno/dom';
import { isDescendant, isInSvg } from '../../tools/utils';
import { Mask } from '../mask/mask';
import { popupStyle } from '../popup/popup.css';
import { DATA } from '../symbol';

interface ActionHandler {
	readonly current: unknown;
	close(): void;
}

export interface DialogEventMap {
	open: Event;
	close: CustomEvent;
}
export type DialogEventKey = keyof DialogEventMap;
export type DialogEventHandler<T extends DialogEventKey, O extends Nodes> = (this: O, event: DialogEventMap[T]) => void;
export interface DialogBase {
	on<K extends keyof DialogEventMap>(
		event: K,
		listener: DialogEventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
	on<K extends keyof HTMLElementEventMap>(
		event: K,
		listener: EventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
	off<K extends keyof DialogEventMap>(
		event: K,
		listener: DialogEventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
	off<K extends keyof HTMLElementEventMap>(
		event: K,
		listener: EventHandler<K, this>,
		options?: boolean | AddEventListenerOptions
	): this;
}

const openedDialogs: Nodes[] = [];

function isPopupActive(): boolean {
	let elem = document.activeElement;
	while (elem) {
		if (dom.wrap(elem).class().includes(popupStyle)) {
			return true;
		}
		elem = elem.parentElement;
	}
	return false;
}

function confineFocus() {
	if (openedDialogs.length <= 0) return;
	if (isPopupActive()) return;
	const lastDialog = openedDialogs[openedDialogs.length - 1];
	if (!document.activeElement || !isDescendant(lastDialog.elems(), document.activeElement)) {
		lastDialog.focus();
	}
}

if (document !== undefined) {
	document.documentElement.addEventListener('focusin', confineFocus);
}

function addDialog(dlg: Nodes) {
	if (openedDialogs.length > 0) {
		const last = openedDialogs[openedDialogs.length - 1];
		last.addClass('pre-covered');
		setTimeout(() => {
			last.addClass('covered');
		}, 16);
	}
	openedDialogs.push(dlg);
	confineFocus();
}

function removeDialog(dlg: Nodes) {
	const idx = openedDialogs.findIndex((n) => n === dlg);
	let previous: Nodes | undefined;
	if (idx >= 0) {
		previous = openedDialogs[idx - 1];
		openedDialogs.splice(idx, 1);
	}
	if (openedDialogs.length > 0) {
		const last = openedDialogs[openedDialogs.length - 1];
		if (last === previous) {
			last.removeClass('covered');
			setTimeout(() => {
				last.removeClass('pre-covered');
			}, 190);
		}
	}
}

type OpenDialogOptions = {
	focusElement?: Nodes | HTMLElement;
};

export abstract class DialogBase extends Container {
	protected [DATA]: { visible: boolean; autoClose?: boolean; escClose?: boolean } = {
		visible: false,
		autoClose: false,
		escClose: false,
	};
	protected abstract getPanel(): Nodes;

	static actionHandler?: ActionHandler;

	constructor(elements: Iterable<Element | Text>) {
		super(elements);
		this.on('mousedown', (evt) => {
			if (isInSvg(evt.target as Element)) {
				return;
			}
			const isParent = !isDescendant(this.getPanel().elems(), evt.target as HTMLElement);
			if (isParent && this.autoClose()) {
				this.close();
			}
		});
		this.getPanel().on('keydown', (evt) => {
			if (evt.key === 'Escape' && (this.autoClose() || this.escClose())) {
				this.close();
			}
		});
	}

	show(options?: OpenDialogOptions): this {
		if (this[DATA].visible) {
			return this;
		}
		DialogBase.actionHandler?.current && DialogBase.actionHandler.close();
		this[DATA].visible = true;
		Mask.show();
		this.appendTo(document.body);
		requestAnimationFrame(() => {
			this.addClass('active');
		});
		setTimeout(() => {
			this.emit(new Event('open'));
			if (options?.focusElement) {
				options?.focusElement.focus();
			} else {
				this.getPanel().focus();
			}
		}, 16);
		addDialog(this);
		return this;
	}

	close(result?: unknown): this {
		if (!this[DATA].visible) {
			return this;
		}
		this[DATA].visible = false;
		this.removeClass('active');
		setTimeout(() => {
			this.detach();
			this.emit(new CustomEvent('close', { detail: result }));
		}, 200);
		removeDialog(this);
		Mask.close();
		return this;
	}

	autoClose(): boolean;
	autoClose(value: boolean): this;
	autoClose(value?: boolean): this | boolean {
		if (value === undefined) {
			return this[DATA]?.autoClose ?? false;
		}
		this[DATA].autoClose = value;
		return this;
	}

	escClose(): boolean;
	escClose(value: boolean): this;
	escClose(value?: boolean): this | boolean {
		if (value === undefined) {
			return this[DATA]?.escClose ?? false;
		}
		this[DATA].escClose = value;
		return this;
	}

	override focus(options?: FocusOptions | undefined): this {
		this.getPanel().focus(options);
		return this;
	}
}
